import { GameObjects, Math, Scene } from 'phaser';
import blockTypeArray from './BlockType';

import Sprite = GameObjects.Sprite;

const MakeKey = (x: number, y: number) => x * 1000 + y;

const IterateBlock = (
  idx: number,
  trans: number,
  callback: (x: number, y: number, data: number) => boolean,
) => {
  const transArray = blockTypeArray[idx];
  const blockArray = transArray[trans];
  for (let y = 0; y < 4; y++) {
    const xdata = blockArray[y];
    for (let x = 0; x < 4; x++) {
      const data = xdata[x];
      if (callback && !callback(x, y, data)) {
        return false;
      }
    }
  }
  return true;
};

interface BlockConfig {
  grid: { size: number; width: number; height: number };
  key: string[];
  position?: { x: number; y: number };
}

export class Block {
  config: BlockConfig;
  blockGroup: GameObjects.Group;

  blockType!: number;
  blockStatus!: number;
  blockPosX!: number;
  blockPosY!: number;

  map: { [key: number]: Sprite } = {};

  moveSpeed = 100;
  dropSpeed = 1000;
  moveTime = 0;
  dropTime = 0;

  constructor(scene: Scene, config: BlockConfig) {
    this.blockGroup = scene.add.group();
    this.config = config;

    const { grid } = this.config;
    const { size, width, height } = grid;

    for (let y = 0; y < height; y++) {
      for (let x = 0; x < width; x++) {
        if (!this.checkBorder(x, y)) {
          const block = this.blockGroup.create(x * size, y * size);
          block.setOrigin(0).setVisible(false);
          this.map[MakeKey(x, y)] = block;
        }
      }
    }
  }

  DropTime(time: number) {
    if (time >= this.dropTime) {
      this.dropTime = time + this.dropSpeed;
      return true;
    }
    return false;
  }

  MoveTime(time: number) {
    if (time >= this.moveTime) {
      this.moveTime = time + this.moveSpeed;
      return true;
    }
    return false;
  }

  create() {
    this.blockType = Math.Between(0, 6);
    const transArray = blockTypeArray[this.blockType];
    this.blockStatus = Math.Between(0, transArray.length - 1);
    const blockArray = transArray[this.blockStatus];
    this.blockPosX = 4;
    this.blockPosY = blockArray[0].join('') === '0000' ? -1 : 0;
  }

  place() {
    return this.placeBlock(this.blockType, this.blockStatus, this.blockPosX, this.blockPosY);
  }

  clear() {
    IterateBlock(this.blockType, this.blockStatus, (x, y, data) => {
      if (data) {
        const finalX = x + this.blockPosX;
        const finalY = y + this.blockPosY;
        this.setBlock(finalX, finalY, '', false);
      }
      return true;
    });
  }

  move(deltaX: number, deltaY: number) {
    this.clear();

    const finalX = this.blockPosX + deltaX;
    const finalY = this.blockPosY + deltaY;

    if (this.placeBlock(this.blockType, this.blockStatus, finalX, finalY)) {
      this.blockPosX = finalX;
      this.blockPosY = finalY;
      return true;
    }

    this.place();
    return false;
  }

  rotate() {
    this.clear();

    let trans = this.blockStatus + 1;
    if (trans === blockTypeArray[this.blockType].length) {
      trans = 0;
    }
    if (this.placeBlock(this.blockType, trans, this.blockPosX, this.blockPosY)) {
      this.blockStatus = trans;
    } else {
      this.place();
    }
  }

  shift() {
    const { grid } = this.config;
    const { height } = grid;
    for (let y = 1; y < height - 1; y++) {
      if (this.checkEmptyLine(y) && !this.checkEmptyLine(y - 1)) {
        this.moveDown(y);
      }
    }
  }

  moveDown(sweepY: number) {
    const { grid } = this.config;
    const { width } = grid;
    for (let y = sweepY; y > 0; y--) {
      for (let x = 1; x < width - 1; x++) {
        const block = this.map[MakeKey(x, y - 1)];
        this.setBlock(x, y, block.texture.key, block.visible);
      }
    }
  }

  placeBlock(idx: number, trans: number, newX: number, newY: number) {
    const { key } = this.config;
    const result: { finalX: number; finalY: number }[] = [];

    const success = IterateBlock(idx, trans, (x, y, data) => {
      if (data) {
        const finalX = x + newX;
        const finalY = y + newY;
        if (this.checkBorder(finalX, finalY) || this.getBlock(finalX, finalY)) {
          return false;
        }
        result.push({ finalX, finalY });
      }
      return true;
    });

    if (success) {
      result.forEach(({ finalX, finalY }) => this.setBlock(finalX, finalY, key[idx], true));
    }
    return success;
  }

  clearLine(y: number) {
    const { grid } = this.config;
    const { width } = grid;
    for (let x = 1; x < width - 1; x++) {
      this.setBlock(x, y, '', false);
    }
  }

  checkBorder(x: number, y: number) {
    const { grid } = this.config;
    const { width, height } = grid;
    return x === 0 || x === width - 1 || y === height - 1;
  }

  checkFullLine(y: number) {
    const { grid } = this.config;
    const { width } = grid;
    for (let x = 1; x < width - 1; x++) {
      if (!this.getBlock(x, y)) {
        return false;
      }
    }
    return true;
  }

  checkEmptyLine(y: number) {
    const { grid } = this.config;
    const { width } = grid;
    for (let x = 1; x < width - 1; x++) {
      if (this.getBlock(x, y)) {
        return false;
      }
    }
    return true;
  }

  checkAndSweep() {
    const { grid } = this.config;
    const { height } = grid;
    let count = 0;
    for (let y = 0; y < height - 1; y++) {
      if (this.checkFullLine(y)) {
        this.clearLine(y);
        count += 1;
      }
    }
    return count;
  }

  getBlock(x: number, y: number) {
    const block = this.map[MakeKey(x, y)];
    if (block) {
      return block.visible;
    }
    return undefined;
  }

  setBlock(x: number, y: number, key: string, value: boolean) {
    const block = this.map[MakeKey(x, y)];
    block && block.setTexture(key).setVisible(value);
  }
}
